package org.xenei.contracts.maven;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.repository.RepositorySystem;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
import org.codehaus.plexus.util.StringUtils;
import org.xenei.classpathutils.ClassPathFilter;
import org.xenei.classpathutils.ClassPathUtils;
import org.xenei.classpathutils.filter.NotClassFilter;
import org.xenei.classpathutils.filter.parser.Parser;
import org.xenei.junit.contract.Contract;
import org.xenei.junit.contract.ContractImpl;
import org.xenei.junit.contract.NoContractTest;
import org.xenei.junit.contract.tooling.InterfaceInfo;
import org.xenei.junit.contract.tooling.InterfaceReport;
/**
* Generate contract test reports.
*
*/
@Mojo(name = "contract-test", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES, requiresDependencyResolution = ResolutionScope.TEST)
public class ContractMojo extends AbstractMojo {
/**
* A list of packages to process. Includes sub packages.
*/
@Parameter
private String[] packages;
/**
* A filter expression for classes to skip.
*/
@Parameter()
private String skipFilter;
/**
* The filter generated by the above string. This is the filter of classes
* to keep expressed as NOT( filterString ).
*/
private ClassPathFilter filter = ClassPathFilter.TRUE;
/**
* Report configuration for untested interfaces. Untested interfaces are
* interfaces that are defined in the list of packages but that do not have
* contract tests and are not annotated with NoContractTest.
*/
@Parameter
private ReportConfig untested;
/**
* Report configuration for unimplemented tests. Unimplemented tests are
* classes that implement an interface that has a Contract test but for
* which no contract suite test implementation is found.
*/
@Parameter
private ReportConfig unimplemented;
/**
* If there are errors in the ContractMojo should the build be failed.
* Defaults to true;
*/
@Parameter
private boolean failOnError = true;
/**
* Report configuration for errors generated during run.
*/
@Parameter
private ReportConfig errors;
@Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true)
private File classDir;
@Parameter(defaultValue = "${project.build.testOutputDirectory}", readonly = true)
private File testDir;
@Parameter(defaultValue = "${project.build.directory}", readonly = true)
private File target;
@Component
private MavenProject project;
@Parameter(defaultValue = "${plugin.artifactMap}", required = true, readonly = true)
private Map<String, Artifact> pluginArtifactMap;
@Component
private RepositorySystem repositorySystem;
@Parameter(defaultValue = "${localRepository}", required = true, readonly = true)
private ArtifactRepository localRepository;
private Set<Artifact> junitContractsArtifacts;
private File myDir;
private final StringBuilder failureMessage = new StringBuilder();
public ContractMojo() {
}
public void setPackages(final String[] packages) {
this.packages = packages;
}
public void setSkipFilter(final String filter)
throws MojoExecutionException {
if (StringUtils.isBlank(filter)) {
this.filter = ClassPathFilter.TRUE;
} else {
try {
this.filter = new NotClassFilter(new Parser().parse(filter));
} catch (IllegalArgumentException e) {
throw new MojoExecutionException(String.format(
"Could not create parse filter: %s", filter,
e.getMessage()), e);
}
}
}
public void setErrors(final ReportConfig errors) {
this.errors = errors;
}
public void setUntested(ReportConfig untested) {
this.untested = untested;
}
public void setUnimplemented(ReportConfig unimplemented) {
this.unimplemented = unimplemented;
}
/**
* If true the build will fail if there is an error in the mojo. Defaults to
* <code>true</code>
*
* @param failOnError
* if true the build will fail on error.
*/
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
private void mojoError( String err ) throws MojoExecutionException
{
if (failOnError)
{
getLog().error(err);
throw new MojoExecutionException(err);
}
getLog().info(err);
}
private void mojoError( String err, Throwable throwable ) throws MojoExecutionException
{
if (failOnError)
{
getLog().error(err, throwable);
throw new MojoExecutionException(err, throwable);
}
getLog().info(err, throwable);
}
@Override
public void execute() throws MojoExecutionException {
boolean success = true;
try {
if ((packages == null) || (packages.length == 0)) {
mojoError( "At least one package must be specified");
return;
}
if (getLog().isInfoEnabled()) {
for (final String s : packages) {
getLog().info("Processing package: " + s);
}
getLog().info("Skip filter: " + filter);
}
myDir = new File(target, "contract-reports");
if (!myDir.exists()) {
myDir.mkdirs();
}
InterfaceReport ir;
try {
ir = new InterfaceReport(packages, filter, buildClassLoader());
} catch (IllegalArgumentException e1) {
mojoError("Could not create Interface report class", e1);
return;
}
doReportInterfaces(ir);
success &= doReportUntested(ir.getUntestedInterfaces());
success &= doReportUnimplemented(ir.getUnImplementedTests());
success &= doReportErrors(ir.getErrors());
if (!success) {
mojoError(failureMessage.toString());
}
} catch (RuntimeException e) {
mojoError(e.getMessage(), e);
}
}
private void addFailureMessage(final String msg) {
addFailureMessage(msg, null);
}
private void addFailureMessage(final String msg, final Exception e) {
if (failureMessage.length() > 0) {
failureMessage.append(System.getProperty("line.separator"));
}
if (e == null) {
getLog().warn(msg);
} else {
getLog().warn(msg, e);
}
failureMessage.append(msg);
}
private void doReportInterfaces(final InterfaceReport ir) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(new File(myDir,
"interfaces.txt")));
bw.write(String.format("Filter: %s", ir.getClassFilter()));
bw.newLine();
bw.newLine();
bw.write("A list of all interfaces that meet the filter and their contract tests");
bw.newLine();
bw.write("----------------------------------------------------------------------");
bw.newLine();
bw.newLine();
for (final InterfaceInfo ii : ir.getInterfaceInfoCollection()) {
if (ir.getClassFilter().accept(ii.getName())) {
final String entry = String.format("Interface: %s %s", ii
.getName().getName(), ii.getTests());
if (getLog().isDebugEnabled()) {
getLog().debug(entry);
}
bw.write(entry);
bw.newLine();
}
}
bw.newLine();
bw.write("A list of all classes that meet the filter");
bw.newLine();
bw.write("------------------------------------------");
bw.newLine();
bw.newLine();
for (final Class<?> cls : ir.getClassFilter().filterClasses(
ir.getPackageClasses())) {
final String entry = String.format(
"Class: %s, contract: %s, impl: %s, flg: %s, all: %s",
cls.getName(),
cls.getAnnotation(Contract.class) != null,
cls.getAnnotation(ContractImpl.class) != null,
cls.getAnnotation(NoContractTest.class) != null,
Arrays.asList(cls.getAnnotations()));
if (getLog().isDebugEnabled()) {
getLog().debug(entry);
}
bw.write(entry);
bw.newLine();
}
} catch (final IOException e) {
getLog().warn(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(bw);
}
}
private boolean doReportUntested(final Set<Class<?>> untestedInterfaces) {
if (!untestedInterfaces.isEmpty()) {
if (untested.isReporting()) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(new File(myDir,
"untested.txt")));
bw.write("Interfaces that are defined in the list of packages but that");
bw.newLine();
bw.write("do not have contract tests and are not annotated with NoContractTest.");
bw.newLine();
bw.write("---------------------------------------------------------------------");
bw.newLine();
bw.newLine();
bw.write(String.format("Filter: %s", untested.getFilter()));
bw.newLine();
bw.newLine();
for (final Class<?> c : untested.getFilter().filterClasses(
untestedInterfaces)) {
bw.write(c.getName());
bw.newLine();
}
} catch (final IOException e) {
addFailureMessage("Unable to write untested report", e);
return false;
} finally {
IOUtils.closeQuietly(bw);
}
}
if (untested.isFailOnError()
&& !untested.getFilter().filterClasses(untestedInterfaces)
.isEmpty()) {
addFailureMessage("Untested Interfaces Exist");
return false;
}
}
return true;
}
private boolean doReportUnimplemented(final Set<Class<?>> unimplementedTests) {
if (!unimplementedTests.isEmpty()) {
if (unimplemented.isReporting()) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(new File(myDir,
"unimplemented.txt")));
bw.write("Classes that implement an interface that has a Contract test");
bw.newLine();
bw.write("but for which no contract suite test implementation is found.");
bw.newLine();
bw.write("-------------------------------------------------------------");
bw.newLine();
bw.newLine();
bw.write(String.format("Filter: %s",
unimplemented.getFilter()));
bw.newLine();
bw.newLine();
for (final Class<?> c : unimplemented.getFilter().filterClasses(
unimplementedTests)) {
bw.write(c.getName());
bw.newLine();
}
} catch (final IOException e) {
addFailureMessage("Unable to write unimplemented report", e);
return false;
} finally {
IOUtils.closeQuietly(bw);
}
}
if (unimplemented.isFailOnError()
&& !unimplemented.getFilter().filterClasses(unimplementedTests)
.isEmpty()) {
addFailureMessage("Unimplemented Tests Exist");
return false;
}
}
return true;
}
private boolean doReportErrors(final List<Throwable> errorLst) {
if (!errorLst.isEmpty()) {
if (errors.isReporting()) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(new File(myDir,
"errors.txt")));
for (final Throwable t : errorLst) {
bw.write(t.toString());
bw.newLine();
}
} catch (final IOException e) {
addFailureMessage("Unable to write error report", e);
return false;
} finally {
IOUtils.closeQuietly(bw);
}
}
if (errors.isFailOnError()) {
addFailureMessage("Contract Test Errors Exist");
return false;
}
}
return true;
}
private ClassLoader buildClassLoader() throws MojoExecutionException {
final ClassWorld world = new ClassWorld();
ClassRealm realm;
try {
realm = world.newRealm("contract", null);
// add contract test and it's transient dependencies.
for (final Artifact elt : getJunitContractsArtifacts()) {
final String dir = String.format("%s!/", elt.getFile().toURI()
.toURL());
if (getLog().isDebugEnabled()) {
getLog().debug("Checking for imports from: " + dir);
}
try {
final Set<String> classNames = ClassPathUtils.findClasses(
dir, "org.xenei.junit.contract");
for (final String clsName : classNames) {
if (getLog().isDebugEnabled()) {
getLog().debug(
"Importing from current classloader: "
+ clsName);
}
importFromCurrentClassLoader(realm,
Class.forName(clsName));
}
} catch (final ClassNotFoundException e) {
throw new MojoExecutionException(e.toString(), e);
} catch (final IOException e) {
throw new MojoExecutionException(e.toString(), e);
}
}
// add source dirs
for (final String elt : project.getCompileSourceRoots()) {
final URL url = new File(elt).toURI().toURL();
realm.addURL(url);
if (getLog().isDebugEnabled()) {
getLog().debug("Source root: " + url);
}
}
// add Compile classpath
for (final String elt : project.getCompileClasspathElements()) {
final URL url = new File(elt).toURI().toURL();
realm.addURL(url);
if (getLog().isDebugEnabled()) {
getLog().debug("Compile classpath: " + url);
}
}
// add Test classpath
for (final String elt : project.getTestClasspathElements()) {
final URL url = new File(elt).toURI().toURL();
realm.addURL(url);
if (getLog().isDebugEnabled()) {
getLog().debug("Test classpath: " + url);
}
}
} catch (final DuplicateRealmException e) {
throw new MojoExecutionException(e.getMessage(), e);
} catch (final MalformedURLException e) {
throw new MojoExecutionException(e.getMessage(), e);
} catch (final DependencyResolutionRequiredException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
return realm;
}
private void importFromCurrentClassLoader(final ClassRealm realm,
final Class<?> cls) {
if (cls == null) {
return;
}
realm.importFrom(Thread.currentThread().getContextClassLoader(),
cls.getName());
// ClassRealm importing is prefix-based, so no need to specifically add
// inner classes
for (final Class<?> intf : cls.getInterfaces()) {
importFromCurrentClassLoader(realm, intf);
}
importFromCurrentClassLoader(realm, cls.getSuperclass());
}
private Set<Artifact> getJunitContractsArtifacts() {
if (junitContractsArtifacts == null) {
final ArtifactResolutionRequest request = new ArtifactResolutionRequest()
.setArtifact(
pluginArtifactMap.get("org.xenei:junit-contracts"))
.setResolveTransitively(true)
.setLocalRepository(localRepository);
final ArtifactResolutionResult result = repositorySystem
.resolve(request);
junitContractsArtifacts = result.getArtifacts();
}
return junitContractsArtifacts;
}
}